第1章 关于this
this
既不指向函数自身也不指向函数的词法作用域this
实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。this和词法作用域是不一样的,不能混合使用。- 当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
1 | // 使用this,提供一种优雅的方式隐式“传递”一个对象引用。API设计更加简洁并且易于复用。 |
第2章 this全面解析
2.1 调用位置
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
查找方法:
分析调用栈:调用位置就是当前正在执行的函数的前一个调用中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar的调用位置
}
function bar() {
// 当前调用栈是:baz --> bar
// 因此,当前调用位置在baz中
console.log( "bar" );
foo(); // <-- foo的调用位置
}
function foo() {
// 当前调用栈是:baz --> bar --> foo
// 因此,当前调用位置在bar中
console.log( "foo" );
}
baz(); // <-- baz的调用位置
使用开发者工具得到调用栈:
设置断点或者插入
debugger;
语句,运行时调试器会在那个位置暂停,同时展示当前位置的函数调用列表,这就是调用栈。找到栈中的第二个元素,这就是真正的调用位置。
2.2 绑定规则
2.2.1 默认绑定
- 独立函数调用,可以把默认绑定看作是无法应用其他规则时的默认规则,this指向全局对象。
- 严格模式下,不能将全局对象用于默认绑定,this会绑定到
undefined
。只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。在严格模式下调用函数则不影响默认绑定。
1 | function foo() { // 运行在严格模式下,this会绑定到undefined |
2.2.2 隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象。对象属性引用链中只有上一层或者说最后一层在调用中起作用。
1 | function foo() { |
隐式丢失
被隐式绑定的函数特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。
1 | // 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身。 |
参数传递就是一种隐式赋值,传入函数时也会被隐式赋值。回调函数丢失this绑定是非常常见的。
1 | function foo() { |
2.2.3 显式绑定
通过call(..)
或者 apply(..)
方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this。因为直接指定this的绑定对象,称之为显示绑定。
1 | function foo() { |
显示绑定无法解决丢失绑定问题。
解决方案:
- 1、硬绑定
创建函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。
1 | function foo() { |
典型应用场景是创建一个包裹函数,负责接收参数并返回值。
1 | function foo(something) { |
创建一个可以重复使用的辅助函数。
1 | function foo(something) { |
ES5内置了Function.prototype.bind
,bind会返回一个硬绑定的新函数,用法如下。
1 | function foo(something) { |
- 2、API调用的“上下文”
JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(..)
一样,确保回调函数使用指定的this。这些函数实际上通过call(..)
和apply(..)
实现了显式绑定。
1 | function foo(el) { |
2.2.4 new绑定
- 在JS中,
构造函数
只是使用new
操作符时被调用的普通
函数,他们不属于某个类,也不会实例化一个类。 - 包括内置对象函数(比如
Number(..)
)在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。 - 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 1、创建(或者说构造)一个新对象。
- 2、这个新对象会被执行[[Prototype]]连接。
- 3、这个新对象会绑定到函数调用的this。
- 4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
使用new
来调用foo(..)
时,会构造一个新对象并把它(bar)绑定到foo(..)
调用中的this。
1 | function foo(a) { |
2.3 优先级
1 | st=>start: Start |
在new
中使用硬绑定函数的目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数(柯里化)。
1 | function foo(p1, p2) { |
2.4 绑定例外
2.4.1 被忽略的this
把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认规则。
下面两种情况下会传入null
- 使用
apply(..)
来“展开”一个数组,并当作参数传入一个函数 bind(..)
可以对参数进行柯里化(预先设置一些参数)
1 | function foo(a, b) { |
总是传入null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。
更安全的this
安全的做法就是传入一个特殊的对象(空对象),把this绑定到这个对象不会对你的程序产生任何副作用。
JS中创建一个空对象最简单的方法是Object.create(null),这个和{}很像,但是并不会创建Object.prototype
这个委托,所以比{}更空。
1 | function foo(a, b) { |
2.4.2 间接引用
间接引用下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。
1 | // p.foo = o.foo的返回值是目标函数的引用,所以调用位置是foo()而不是p.foo()或者o.foo() |
2.4.3 软绑定
- 硬绑定可以把this强制绑定到指定的对象(new除外),防止函数调用应用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this。
- 如果给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
1 | // 默认绑定规则,优先级排最后 |
使用:软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。
1 | function foo() { |
2.5 this词法
ES6新增一种特殊函数类型:箭头函数,箭头函数无法使用上述四条规则,而是根据外层(函数或者全局)作用域(词法作用域)来决定this.
- foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不行)。
1 | function foo() { |
ES6之前和箭头函数类似的模式,采用的是词法作用域取代了传统的this机制。
1 | function foo() { |
代码风格统一问题:如果既有this风格的代码,还会使用 seft = this
或者箭头函数来否定this机制。
- 只使用词法作用域并完全抛弃错误this风格的代码;
- 完全采用this风格,在必要时使用
bind(..)
,尽量避免使用self = this
和箭头函数。